ExpressBeans   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 163
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 23
eloc 94
dl 0
loc 163
c 0
b 0
f 0
ccs 55
cts 55
cp 1
rs 10

8 Functions

Rating   Name   Duplication   Size   Complexity  
A createApp 0 8 1
A registerRouters 0 24 2
A listen 0 14 3
C serializeRequest 0 16 10
A checkRouterBeans 0 17 2
A getApp 0 7 1
A use 0 7 1
A initialize 0 27 3
1 9
import express, { Express, Request, Router } from 'express';
2 9
import { pinoHttp, startTime } from 'pino-http';
3
import { ServerResponse, IncomingMessage } from 'http';
4 9
import EventEmitter from 'events';
5
import { ExpressBeansOptions, ExpressRouterBean } from '@/ExpressBeansTypes';
6 9
import { logger } from '@/core';
7 9
import { Executor } from '@/core/executor';
8 9
import { Objects } from '@/utils/Objects';
9
10
type ExpressBeanEventMap = {
11
  error: [Error];
12
  initialized: [];
13
}
14
15 9
export default class ExpressBeans extends EventEmitter<ExpressBeanEventMap> {
16 23
  private readonly app: Express;
17
18 23
  private readonly router: Router;
19
20
  /**
21
   * Creates a new ExpressBeans application
22
   * @param options {ExpressBeansOptions}
23
   */
24
  static async createApp(options?: Partial<ExpressBeansOptions>): Promise<ExpressBeans> {
25 6
    const app = new ExpressBeans({ ...options });
26 6
    return app;
27
  }
28
29
  /**
30
   * Constructor of ExpressBeans application
31
   * @constructor
32
   * @param options {ExpressBeansOptions}
33
   */
34
  constructor(options?: Partial<ExpressBeansOptions>) {
35 23
    super();
36 23
    this.router = express.Router();
37 23
    this.app = express();
38 23
    this.app.disable('x-powered-by');
39 23
    this.app.use(options?.baseURL ?? '/', this.router);
40 23
    if (options?.logRequests === undefined || options.logRequests) {
41 21
      this.router.use(pinoHttp(
42
        {
43
          logger,
44
          customSuccessMessage: this.serializeRequest.bind(this),
45
          customErrorMessage: this.serializeRequest.bind(this),
46
        },
47
      ));
48
    }
49 23
    Executor.setExecution('run', () => this.initialize(options ?? {}));
50 23
    Executor.on('error', (error) => {
51 6
      logger.error(error);
52 6
      process.exit(1);
53
    });
54 23
    Executor.startLifecycle();
55
  }
56
57
  private serializeRequest(req: IncomingMessage, res: ServerResponse) {
58 14
    const request: Request = req as Request;
59 14
    const remoteAddress = request.headers['x-forwarded-for'] ?? request.socket.remoteAddress;
60 14
    const { method, originalUrl, httpVersion } = request;
61 14
    const responseTime = Date.now() - res[startTime];
62 14
    const optionals = [
63
      res.statusCode,
64
      res.getHeader('content-length'),
65
      res.getHeader('content-type'),
66
      request.headers.referer,
67
      request.headers['user-agent'],
68
    ]
69
      .filter(Objects.nonNullish)
70
      .join(' ');
71 14
    return `${remoteAddress} - "${method} ${originalUrl} HTTP/${httpVersion}" ${optionals} - ${responseTime}ms`;
72
  }
73
74
  /**
75
   * Initializes the application and checks
76
   * if all beans are valid
77
   * @param listen {boolean}
78
   * @param port {number}
79
   * @param beans {Object[]}
80
   * @param onInitialized {Function}
81
   * @private
82
   */
83 20
  private async initialize({
84
    listen = true,
85
    port = 8080,
86
    routerBeans = [],
87
  }: Partial<ExpressBeansOptions>) {
88 20
    return Promise.resolve(routerBeans as Array<ExpressRouterBean>)
89
      .then(this.checkRouterBeans.bind(this))
90
      .then(this.registerRouters.bind(this))
91
      .then(() => {
92 18
        if (listen) {
93 6
          return new Promise<void>(
94
            (resolve, reject) => {
95 6
              this.listen(port, (err) => (err ? reject(err) : resolve()));
96
            },
97 4
          ).catch((err: Error) => Promise.reject(err));
98
        }
99 12
        return Promise.resolve();
100
      });
101
  }
102
103
  /**
104
   * Starts the server and emits the initialized event
105
   * @param {number} port
106
   */
107
  listen(port: number, callback?: (error?: Error) => void) {
108 17
    return this.app.listen(port, (error) => {
109 17
      if (error) {
110 4
        callback?.(error);
111 4
        this.emit('error', error);
112 1
        return;
113
      }
114 13
      logger.info(`Server listening on port ${port}`);
115 13
      this.emit('initialized');
116
    });
117
  }
118
119
  /**
120
   * Registers all routers
121
   * @param routers {Array<ExpressRouterBean>}
122
   * @private
123
   */
124
  private async registerRouters(routers: Array<ExpressRouterBean>) {
125 19
    return new Promise((resolve, reject) => {
126 19
      Array.from(routers)
127 17
        .map((bean) => bean._instance)
128
        .forEach((instance) => {
129 17
          try {
130
            const {
131
              path,
132
              router,
133 17
            } = instance._routerConfig;
134 17
            logger.debug(`Registering router ${instance._className}`);
135 17
            this.router.use(path, router);
136
          } catch (e) {
137 1
            logger.error(e);
138 1
            reject(new Error(`Router ${instance._className} not initialized correctly`, { cause: e }));
139
          }
140
        });
141 19
      resolve(true);
142
    });
143
  }
144
145
  /**
146
   * Checks if all beans are valid
147
   * @param routerBeans {Array<ExpressRouterBean>}
148
   * @returns {Array<ExpressRouterBean>}
149
   * @throws {Error}
150
   * @private
151
   */
152
  private async checkRouterBeans(routerBeans: Array<ExpressRouterBean>):
153
  Promise<ExpressRouterBean[]> {
154 20
    const invalidBeans = routerBeans
155 20
      .filter(((bean) => !bean._beanUUID))
156 1
      .map((object: any) => object.prototype.constructor.name);
157 20
    if (invalidBeans.length > 0) {
158 1
      return Promise.reject(new Error(`Trying to use something that is not an ExpressBean: ${invalidBeans.join(', ')}`));
159
    }
160 19
    return routerBeans;
161
  }
162
163
  /**
164
   * Gets Express application
165
   * @returns {Express}
166
   */
167
  getApp() {
168 2
    return this.app;
169
  }
170
171
  /**
172
   * Exposes use function of Express application
173
   * @param handlers
174
   */
175
  use(...handlers: any) {
176 1
    this.app.use(...handlers);
177
  }
178
}
179